<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <p data-lake-id="u1cd53c00" id="u1cd53c00"><span data-lake-id="ued5b8f81" id="ued5b8f81">事务管理在系统开发中是不可缺少的一部分，Spring提供了很好事务管理机制，主要分为编程式事务和声明式事务两种。</span></p>
  <p data-lake-id="u99cf6e86" id="u99cf6e86"><br></p>
  <p data-lake-id="uc40e2612" id="uc40e2612"><span data-lake-id="ubc49fe51" id="ubc49fe51">关于事务的基础知识，如什么是事务，数据库事务以及Spring事务的ACID、隔离级别、传播机制、行为等，就不在这篇文章中详细介绍了。默认大家都有一定的了解。</span></p>
  <p data-lake-id="u46c5a61e" id="u46c5a61e"><br></p>
  <p data-lake-id="u8489346a" id="u8489346a"><span data-lake-id="u28dd3b92" id="u28dd3b92">本文，作者会先简单介绍下什么是声明式事务和编程式事务，再说一下为什么我不建议使用声明式事务。</span></p>
  <p data-lake-id="u90ee6d9f" id="u90ee6d9f"><br></p>
  <h3 data-lake-id="3bd0ee3e" id="3bd0ee3e"><span data-lake-id="uf9425891" id="uf9425891">编程式事务</span></h3>
  <p data-lake-id="u77c5aaf6" id="u77c5aaf6"><br></p>
  <p data-lake-id="u77c94b05" id="u77c94b05"><span data-lake-id="u36af7201" id="u36af7201">基于底层的API，如PlatformTransactionManager、TransactionDefinition 和 TransactionTemplate 等核心接口，开发者完全可以通过编程的方式来进行事务管理。</span></p>
  <p data-lake-id="u740c4d88" id="u740c4d88"><br></p>
  <p data-lake-id="u5aae7cbd" id="u5aae7cbd"><strong><span data-lake-id="uf227c24b" id="uf227c24b">编程式事务方式需要是开发者在代码中手动的管理事务的开启、提交、回滚等操作。</span></strong></p>
  <p data-lake-id="u2fdfd30e" id="u2fdfd30e"><br></p>
  <pre lang="java"><code>
public void test() {
      TransactionDefinition def = new DefaultTransactionDefinition();
      TransactionStatus status = transactionManager.getTransaction(def);
      
       try {
         // 事务操作
         // 事务提交
         transactionManager.commit(status);
      } catch (DataAccessException e) {
         // 事务提交
         transactionManager.rollback(status);
         throw e;
      }
}
</code></pre>
  <p data-lake-id="ue927c717" id="ue927c717"><br></p>
  <p data-lake-id="u6b28c7e2" id="u6b28c7e2"><span data-lake-id="ub983a9c8" id="ub983a9c8">如以上代码，开发者可以通过API自己控制事务。</span></p>
  <p data-lake-id="ue659e7ff" id="ue659e7ff"><br></p>
  <h3 data-lake-id="1e2d1932" id="1e2d1932"><span data-lake-id="ub18a6b9a" id="ub18a6b9a">声明式事务</span></h3>
  <p data-lake-id="u800c17e4" id="u800c17e4"><br></p>
  <p data-lake-id="ude456aaa" id="ude456aaa"><strong><span data-lake-id="u3822a388" id="u3822a388">声明式事务管理方法允许开发者配置的帮助下来管理事务，而不需要依赖底层API进行硬编码。开发者可以只使用注解或基于配置的 XML 来管理事务。</span></strong></p>
  <p data-lake-id="u19252ea9" id="u19252ea9"><br></p>
  <pre lang="java"><code>
@Transactional
public void test() {
     // 事务操作  
}
</code></pre>
  <p data-lake-id="ucc3d37ca" id="ucc3d37ca"><br></p>
  <p data-lake-id="u50f366bf" id="u50f366bf"><span data-lake-id="u6d2cddd2" id="u6d2cddd2">如上，使用@Transactional即可给test方法增加事务控制。</span></p>
  <p data-lake-id="u24d17487" id="u24d17487"><br></p>
  <p data-lake-id="ufbc7cec2" id="ufbc7cec2"><span data-lake-id="ue38c08e6" id="ue38c08e6">当然，上面的代码只是简化后的，想要使用事务还需要一些配置内容。这里就不详细阐述了。</span></p>
  <p data-lake-id="u1fd386c2" id="u1fd386c2"><br></p>
  <p data-lake-id="u3b187a35" id="u3b187a35"><span data-lake-id="ud4e9d0bd" id="ud4e9d0bd">这两种事务，各自有各自的优缺点，那么，各自有哪些适合的场景呢？为什么有人会拒绝使用声明式事务呢？</span></p>
  <p data-lake-id="ue1b9d4fd" id="ue1b9d4fd"><br></p>
  <h3 data-lake-id="564f01f9" id="564f01f9"><span data-lake-id="ua29bd3a4" id="ua29bd3a4">声明式事务的优点</span></h3>
  <p data-lake-id="u7efbcdb4" id="u7efbcdb4"><br></p>
  <p data-lake-id="uecdb4341" id="uecdb4341"><span data-lake-id="u6c2a16f5" id="u6c2a16f5">通过上面的例子，其实我们可以很容易的看出来，声明式事务帮助我们节省了很多代码，他会自动帮我们进行事务的开启、提交以及回滚等操作，把程序员从事务管理中解放出来。</span></p>
  <p data-lake-id="u8d1a3532" id="u8d1a3532"><br></p>
  <p data-lake-id="u31695ffd" id="u31695ffd"><strong><span data-lake-id="uf93d8fa0" id="uf93d8fa0">声明式事务管理使用了 AOP 实现的，本质就是在目标方法执行前后进行拦截。</span></strong><span data-lake-id="u1dbbef74" id="u1dbbef74">在目标方法执行前加入或创建一个事务，在执行方法执行后，根据实际情况选择提交或是回滚事务。</span></p>
  <p data-lake-id="ufbef4cbc" id="ufbef4cbc"><br></p>
  <p data-lake-id="u2b4266ad" id="u2b4266ad"><strong><span data-lake-id="u3e895fe7" id="u3e895fe7">使用这种方式，对代码没有侵入性，方法内只需要写业务逻辑就可以了。</span></strong></p>
  <p data-lake-id="ubc24392b" id="ubc24392b"><br></p>
  <p data-lake-id="uf69205bc" id="uf69205bc"><span data-lake-id="ue16e636e" id="ue16e636e">但是，声明式事务真的有这么好么？倒也不见得。</span></p>
  <p data-lake-id="uaabf96f0" id="uaabf96f0"><br></p>
  <h3 data-lake-id="5bb4a961" id="5bb4a961"><span data-lake-id="uaaebce88" id="uaaebce88">声明式事务的粒度问题</span></h3>
  <p data-lake-id="u5a6223d3" id="u5a6223d3"><br></p>
  <p data-lake-id="u345a9be8" id="u345a9be8"><span data-lake-id="u3187bc60" id="u3187bc60">首先，</span><strong><span data-lake-id="uac1fe09e" id="uac1fe09e">声明式事务有一个局限，那就是他的最小粒度要作用在方法上。</span></strong></p>
  <p data-lake-id="u866aac9f" id="u866aac9f"><br></p>
  <p data-lake-id="u829ccde7" id="u829ccde7"><span data-lake-id="u59a5e2be" id="u59a5e2be">也就是说，如果想要给一部分代码块增加事务的话，那就需要把这个部分代码块单独独立出来作为一个方法。</span></p>
  <p data-lake-id="u65909a66" id="u65909a66"><br></p>
  <p data-lake-id="uffa03af0" id="uffa03af0"><span data-lake-id="u038ec8ee" id="u038ec8ee">但是，正是因为这个粒度问题，本人并不建议过度的使用声明式事务。</span></p>
  <p data-lake-id="u1b794424" id="u1b794424"><br></p>
  <p data-lake-id="uf839f325" id="uf839f325"><span data-lake-id="uaea42841" id="uaea42841">首先，因为声明式事务是通过注解的，有些时候还可以通过配置实现，这就会导致一个问题，那就是这个事务有可能被开发者忽略。</span></p>
  <p data-lake-id="uaf9ddfd1" id="uaf9ddfd1"><br></p>
  <p data-lake-id="uc653d3a8" id="uc653d3a8"><span data-lake-id="ua6978af7" id="ua6978af7">事务被忽略了有什么问题呢？</span></p>
  <p data-lake-id="uc1146f19" id="uc1146f19"><br></p>
  <p data-lake-id="u30bce834" id="u30bce834"><strong><span data-lake-id="ub695dee5" id="ub695dee5">首先，如果开发者没有注意到一个方法是被事务嵌套的，那么就可能会再方法中加入一些如RPC远程调用、消息发送、缓存更新、文件写入等操作。</span></strong></p>
  <p data-lake-id="u11c511b9" id="u11c511b9"><br></p>
  <p data-lake-id="u9555f1c2" id="u9555f1c2"><span data-lake-id="u8929c6b8" id="u8929c6b8">我们知道，这些操作如果被包在事务中，有两个问题：</span></p>
  <p data-lake-id="u6015869d" id="u6015869d"><br></p>
  <p data-lake-id="uf4ad2bcb" id="uf4ad2bcb"><span data-lake-id="ua9c4df11" id="ua9c4df11">1、这些操作自身是无法回滚的，这就会导致数据的不一致。可能RPC调用成功了，但是本地事务回滚了，可是PRC调用无法回滚了。</span></p>
  <p data-lake-id="u65a9cc3f" id="u65a9cc3f"><br></p>
  <p data-lake-id="u9b81308c" id="u9b81308c"><span data-lake-id="uaae06c31" id="uaae06c31">2、在事务中有远程调用，就会拉长整个事务。那么久会导致本事务的数据库连接一直被占用，那么如果类似操作过多，就会导致数据库连接池耗尽。</span></p>
  <p data-lake-id="u1601eaec" id="u1601eaec"><br></p>
  <p data-lake-id="u7b6126f9" id="u7b6126f9"><span data-lake-id="u4c27df59" id="u4c27df59">有些时候，即使没有在事务中进行远程操作，但是有些人还是可能会不经意的进行一些内存操作，如运算。或者如果遇到分库分表的情况，有可能不经意间进行跨库操作。</span></p>
  <p data-lake-id="ub4f0584c" id="ub4f0584c"><br></p>
  <p data-lake-id="uae2e9109" id="uae2e9109"><span data-lake-id="u5efd81c4" id="u5efd81c4">但是如果是编程式事务的话，业务代码中就会清清楚楚看到什么地方开启事务，什么地方提交，什么时候回滚。这样有人改这段代码的时候，就会强制他考虑要加的代码是否应该方法事务内。</span></p>
  <p data-lake-id="udc795269" id="udc795269"><br></p>
  <p data-lake-id="ud2d6fa4d" id="ud2d6fa4d"><span data-lake-id="u51d89b01" id="u51d89b01">有些人可能会说，已经有了声明式事务，但是写代码的人没注意，这能怪谁。</span></p>
  <p data-lake-id="uae19e1fd" id="uae19e1fd"><br></p>
  <p data-lake-id="u81b6c46d" id="u81b6c46d"><span data-lake-id="ud832e7e6" id="ud832e7e6">话虽然是这么说，但是我们还是希望可以通过一些机制或者规范，降低这些问题发生的概率。</span></p>
  <p data-lake-id="u2b0d6df9" id="u2b0d6df9"><br></p>
  <p data-lake-id="uaefb8ddc" id="uaefb8ddc"><span data-lake-id="uc19db9a5" id="uc19db9a5">比如建议大家使用编程式事务，而不是声明式事务。因为，作者工作这么多年来，发生过不止一次开发者没注意到声明式事务而导致的故障。</span></p>
  <p data-lake-id="u8bb10ac5" id="u8bb10ac5"><br></p>
  <p data-lake-id="u0baa780a" id="u0baa780a"><span data-lake-id="u2b8a70b5" id="u2b8a70b5">因为有些时候，声明式事务确实不够明显。</span></p>
  <p data-lake-id="u09a0c1fc" id="u09a0c1fc"><br></p>
  <h3 data-lake-id="98484857" id="98484857"><span data-lake-id="uc11e944f" id="uc11e944f">声明式事务用不对容易失效</span></h3>
  <p data-lake-id="uabb625f0" id="uabb625f0"><br></p>
  <p data-lake-id="ud5ce8b38" id="ud5ce8b38"><span data-lake-id="u98f3921d" id="u98f3921d">除了事务的粒度问题，还有一个问题那就是声明式事务虽然看上去帮我们简化了很多代码，但是一旦没用对，也很容易导致事务失效。</span></p>
  <p data-lake-id="u0fcffcd2" id="u0fcffcd2"><br></p>
  <p data-lake-id="ub1ee04eb" id="ub1ee04eb"><span data-lake-id="u0cf463fc" id="u0cf463fc">如以下几种场景就可能导致声明式事务失效：</span></p>
  <p data-lake-id="ub323b6e6" id="ub323b6e6"><br></p>
  <p data-lake-id="ubd40bc87" id="ubd40bc87"><span data-lake-id="ufc1baa0b" id="ufc1baa0b">1、</span><a href="/Transactional " data-lake-id="u099f3d04" id="u099f3d04"><span data-lake-id="ub47bc925" id="ub47bc925">@Transactional </span></a><span data-lake-id="u67b52193" id="u67b52193"> 应用在非 public 修饰的方法上 <br></span><span data-lake-id="u28ef8e85" id="u28ef8e85">2、</span><a href="/Transactional " data-lake-id="u76df6afa" id="u76df6afa"><span data-lake-id="ubcc4130e" id="ubcc4130e">@Transactional </span></a><span data-lake-id="u238455ab" id="u238455ab"> 注解属性 propagation 设置错误 <br></span><span data-lake-id="u2840d687" id="u2840d687">3、</span><a href="/Transactional " data-lake-id="u2b37dd56" id="u2b37dd56"><span data-lake-id="u53b6a4b9" id="u53b6a4b9">@Transactional </span></a><span data-lake-id="u043f661e" id="u043f661e"> 注解属性 rollbackFor 设置错误 <br></span><span data-lake-id="u897c92ae" id="u897c92ae">4、同一个类中方法调用，导致@Transactional失效<br></span><span data-lake-id="uad6eba6e" id="uad6eba6e">5、异常被catch捕获导致@Transactional失效<br></span><span data-lake-id="ud01ae4f1" id="ud01ae4f1">6、数据库引擎不支持事务</span></p>
  <p data-lake-id="u168713ee" id="u168713ee"><br></p>
  <p data-lake-id="ub027b1c1" id="ub027b1c1"><span data-lake-id="u34dc7708" id="u34dc7708">以上几个问题，如果使用编程式事务的话，很多都是可以避免的。</span></p>
  <p data-lake-id="ud85a439b" id="ud85a439b"><br></p>
  <p data-lake-id="u9ed3f58b" id="u9ed3f58b"><span data-lake-id="u55d87256" id="u55d87256">使用声明事务失效的问题我们发生过很多次。不知道大家有没有遇到过，我是实际遇到过的</span></p>
  <p data-lake-id="uefc1917f" id="uefc1917f"><br></p>
  <p data-lake-id="u59ad0a6c" id="u59ad0a6c"><span data-lake-id="ub0701068" id="ub0701068">因为Spring的事务是基于AOP实现的，但是在代码中，有时候我们会有很多切面，不同的切面可能会来处理不同的事情，多个切面之间可能会有相互影响。</span></p>
  <p data-lake-id="u4066a7cf" id="u4066a7cf"><br></p>
  <p data-lake-id="uf0b54bb8" id="uf0b54bb8"><span data-lake-id="u17261ae9" id="u17261ae9">在之前的一个项目中，我就发现我们的Service层的事务全都失效了，一个SQL执行失败后并没有回滚，排查下来才发现，是因为一位同事新增了一个切面，这个切面里面做个异常的统一捕获，导致事务的切面没有捕获到异常，导致事务无法回滚。</span></p>
  <p data-lake-id="u24fcdc75" id="u24fcdc75"><br></p>
  <p data-lake-id="u2732068b" id="u2732068b"><span data-lake-id="u8c4b99f6" id="u8c4b99f6">这样的问题，发生过不止一次，而且不容易被发现。</span></p>
  <p data-lake-id="u01ecf7a0" id="u01ecf7a0"><br></p>
  <p data-lake-id="ud3e8f2b8" id="ud3e8f2b8"><span data-lake-id="u7329976d" id="u7329976d">很多人还是会说，说到底还是自己能力不行，对事务理解不透彻，用错了能怪谁。</span></p>
  <p data-lake-id="ufcb34ad2" id="ufcb34ad2"><br></p>
  <p data-lake-id="u55480e65" id="u55480e65"><span data-lake-id="u05603425" id="u05603425">但是我还是那句话，</span><strong><span data-lake-id="u75a22398" id="u75a22398">我们确实无法保证所有人的能力都很高</span></strong><span data-lake-id="ufaf289ac" id="ufaf289ac">，也无法要求所有开发者都能不出错。我们能做的就是，尽量可以通过机制或者规范，来避免或者降低这些问题发生的概率。</span></p>
  <p data-lake-id="uf33e65e5" id="uf33e65e5"><br></p>
  <p data-lake-id="u2fbf23a9" id="u2fbf23a9"><span data-lake-id="udec3f72f" id="udec3f72f">其实，如果大家有认真看过阿里巴巴出的那份Java开发手册的话，其实就能发现，其中的很多规约并不是完完全全容易被人理解，有些也比较生硬，但是其实，这些规范都是从无数个坑里爬出来的开发者们总结出来的。</span></p>
  <p data-lake-id="u011cbf63" id="u011cbf63"><br></p>
  <p data-lake-id="u8b4764a4" id="u8b4764a4"><span data-lake-id="ua57f5cc7" id="ua57f5cc7">关于@Transactional的用法，规约中也有提到过，只不过规约中的观点没有我这么鲜明：</span></p>
  <p data-lake-id="ufca5a257" id="ufca5a257"><br></p>
  <h3 data-lake-id="25f9c7fa" id="25f9c7fa"><span data-lake-id="u588a18f4" id="u588a18f4">总结</span></h3>
  <p data-lake-id="u1ca99a3b" id="u1ca99a3b"><br></p>
  <p data-lake-id="ubed15e62" id="ubed15e62"><span data-lake-id="u975717ac" id="u975717ac">最后，相信本文的观点很多人都并不一定认同，很多人会说：Spring官方都推荐无侵入性的声明式事务，你有啥资格出来BB 。</span></p>
  <p data-lake-id="u55dbf077" id="u55dbf077"><br></p>
  <p data-lake-id="u164d5749" id="u164d5749"><span data-lake-id="u9a21a2a7" id="u9a21a2a7">说实话，刚工作的前几年，我也热衷于使用声明式事务，觉得很干净，也很"优雅"。觉得师兄们使用编程式事务多此一举，没有工匠精神。</span></p>
  <p data-lake-id="u992d6e5c" id="u992d6e5c"><br></p>
  <p data-lake-id="u48312ae0" id="u48312ae0"><span data-lake-id="u46aa5bc3" id="u46aa5bc3">但是慢慢的，线上发生过几次问题之后，我们复盘后发现，很多时候你自己写的代码很优雅，这完全没问题。</span></p>
  <p data-lake-id="ufd9267dc" id="ufd9267dc"><br></p>
  <p data-lake-id="ud1313eda" id="ud1313eda"><span data-lake-id="u5746f51a" id="u5746f51a">但是，优雅的同时也带来了一些副作用，师兄们又不能批评我，因为我的用法确实没错...</span></p>
  <p data-lake-id="uf11c87d8" id="uf11c87d8"><br></p>
  <p data-lake-id="u1b176f81" id="u1b176f81"><span data-lake-id="ud24d07b1" id="ud24d07b1">所以，有些事，还是要痛过之后才知道。</span></p>
  <p data-lake-id="u1ffad32d" id="u1ffad32d"><br></p>
  <p data-lake-id="u11b17570" id="u11b17570"><span data-lake-id="u251d0095" id="u251d0095">当然，本文并不要求大家一定要彻底不使用声明式事务，只是建议大家日后在使用事务的时候，能够考虑到本文中提到的观点，然后自行选择。</span></p>
 </body>
</html>